﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ObviousCode.Stately.Exceptions;

namespace ObviousCode.Stately
{    
    public class StateObserver<TState, TData> where TState : struct, IConvertible
    {
        public delegate ITransitionRequest TransitionHandler(Transition<TState> stateRequest, TData data);

        public event EventHandler<EventArgs<Transition<TState>>> TransitionOccured;
        public event EventHandler<EventArgs<Transition<TState>>> HandledTransitionOccured;
        public event EventHandler<EventArgs<Transition<TState>>> UnhandledTransitionOccured;
        public event EventHandler<TransitionExceptionEventArgs<TState, TData>> ExceptionOccuredDuringTransition;
        public event EventHandler<EventArgs<TState, TData>> TransitionsCompleted;
        public event EventHandler<EventArgs> WaitRequestStarted;
        public event EventHandler<EventArgs> HaltRequestIssued;
        public event EventHandler<EventArgs> TransitionResumed;
        public event EventHandler<EventArgs<TState, TData>> ResumeRequestRejected;
        public event EventHandler<EventArgs<TState, TData>> ResumeRequestEnqueued;
        public event EventHandler<EventArgs<TState, TData>> ResumeRequestDequeued;

        Dictionary<Transition<TState>, TransitionHandler> _transitions;
        Dictionary<TState, TransitionHandler> _destinationTransitions;

        Queue<Tuple<TState, TData>> _resumeQueue;

        TState _state;

        object _lockObject = new object();

        public bool IsWaiting { get; private set; }

        public StateObserver(TState defaultState)
        {
            if (!typeof(TState).IsEnum)
            {
                throw new ArgumentException("T must be an enumerated type");
            }

            _resumeQueue = new Queue<Tuple<TState, TData>>();
            _state = defaultState;

            _transitions = new Dictionary<Transition<TState>, TransitionHandler>();
            _destinationTransitions = new Dictionary<TState, TransitionHandler>();

            AllowUnhandledTransitions = false;
            AutoTransitionToErrorState = false;
            AllowDatalessTransitions = true;
            AllowQueuedResumeRequests = true;

            ErrorState = default(TState);
        }

        public TState State
        {
            get
            {
                return _state;
            }
            set
            {
                Transition(value, default(TData));
            }
        }

        public TState PreviousState { get; private set; }

        #region Add Transitions
        public void AddTransition(TState from, TState to, TransitionHandler transitionHandler)
        {
            AddTransition(CreateTransition(from, to), transitionHandler);
        }

        public void AddTransition(Transition<TState> transition, TransitionHandler transitionHandler)
        {
            _transitions[transition] = transitionHandler;
        }

        public void AddTransitions(StateCollection<TState> from, TState to, TransitionHandler transitionHandler)
        {
            from
                .ToList()
                .ForEach(f => AddTransition(f, to, transitionHandler));
        }

        public void AddTransitions(StateCollection<TState> from, StateCollection<TState> to, TransitionHandler transitionHandler)
        {
            AddTransitions(from, to, transitionHandler, true);
        }

        public void AddTransitions(StateCollection<TState> from, StateCollection<TState> to, TransitionHandler transitionHandler, bool excludeSelfReferences)
        {
            from
                .ToList()
                .ForEach(f =>
                    to.ToList()
                    .ForEach(t =>
                    {
                        if ((excludeSelfReferences && !f.Equals(t)) ||
                            (!excludeSelfReferences))
                        {
                            AddTransition(f, t, transitionHandler);
                        }
                    })
                );
        }

        public void AddTransitions(IEnumerable<Transition<TState>> transitions, TransitionHandler transitionRequest)
        {
            transitions
                .ToList()
                .ForEach(t => AddTransition(t, transitionRequest));
        }

        public void AddTransitionTo(TState toState, TransitionHandler transitionHandler)
        {
            _destinationTransitions[toState] = transitionHandler;
        }
        #endregion

        #region Transition Handling

        public void Start(TState initialTransition)
        {
            Start(initialTransition, default(TData));
        }

        public void Start(TState initialTransition, TData data)
        {
            ITransitionRequest request = TransitionRequest<TState, TData>.For(initialTransition, data);

            while (request != null && request.TransitionType == RequestType.TransitionRequest)
            {
                TransitionRequest<TState, TData> r = request as TransitionRequest<TState, TData>;

                request = Transition(r.StateRequest, r.Data);
            }

            if (request == null || request.TransitionType == RequestType.CompleteRequest) { HandleCompleteTransitionRequest(_state, data); }
            else if (request.TransitionType == RequestType.WaitRequest) HandleWaitRequest();
            else if (request.TransitionType == RequestType.HaltRequest) HandleHaltRequest();
        }
        
        public void Resume(TState resumeState)
        {
            Resume(resumeState, default(TData));
        }

        public void Resume(TState resumeState, TData data, TState assertWaitingState)
        {
            if (_state.IsNot(assertWaitingState)) throw new InvalidOperationException("Assert waiting state failed. Expecting state: {0} Actual state: {1}".F(assertWaitingState, _state));

            Resume(resumeState, data);            
        }

        public void Resume(TState resumeState, TData data)
        {
            if (TransitionResumed != null) TransitionResumed(this, EventArgs.Empty);

            lock (_lockObject)
            {
                if (!IsWaiting &&
                    !AllowQueuedResumeRequests)
                {
                    if (ResumeRequestRejected != null)
                    {
                        ResumeRequestRejected(this, new EventArgs<TState, TData>(resumeState, data));
                        return;
                    }
                    else
                    {
                        throw new StatelyProcessException("Resume Request Rejected. Please Handle ResumeRequestRejected event or set AllowQueuedResumeRequests to true");
                    }
                }

                _resumeQueue.Enqueue(new Tuple<TState, TData>(resumeState, data));

                if (IsWaiting)
                {
                    IsWaiting = false;

                    Tuple<TState, TData> nextItem = _resumeQueue.Dequeue();

                    Start(nextItem.Item1, nextItem.Item2);
                }
                else
                {
                    if (ResumeRequestEnqueued != null) ResumeRequestEnqueued(this, new EventArgs<TState, TData>(resumeState, data));
                }
            }
        }

        public void Derail(TState toState, TData data)
        {
            Transition(toState, data);
        }

        public void Derail(TState toState)
        {
            Transition(toState);
        }

        private ITransitionRequest Transition(TState state)
        {
            return Transition(state, default(TData));
        }

        private ITransitionRequest Transition(TState state, TData data)
        {
            Transition<TState> transition = CreateTransition(_state, state);
            
            PreviousState = _state;

            _state = state;

            if (TransitionOccured != null) TransitionOccured(this, new EventArgs<Transition<TState>>(transition));

            if (_transitions.ContainsKey(transition))
            {
                return InvokeTransition(transition, _transitions[transition], data);
            }
            else if (_destinationTransitions.ContainsKey(transition.To))
            {
                return InvokeTransition(transition, _destinationTransitions[transition.To], data);
            }
            else
            {
                HandleUnhandledTransitionAttempt(transition);
                return null;
            }            
        }

        private ITransitionRequest InvokeTransition(Transition<TState> transition, TransitionHandler transitionHandler, TData data)
        {
            if (HandledTransitionOccured != null) HandledTransitionOccured(this, new EventArgs<Transition<TState>>(transition));

            if (transitionHandler == null) { HandleUnhandledTransitionAttempt(transition); return null; }

            try
            {
                if ((data == null || data.Equals(default(TData))) && !AllowDatalessTransitions) throw new InvalidOperationException("Dataless transitions are not allowed");

                return transitionHandler(transition, data);
            }
            catch (Exception e)
            {
                if (e is StatelyProcessException) throw e;

                if (ExceptionOccuredDuringTransition != null)
                {
                    ExceptionOccuredDuringTransition(this, new TransitionExceptionEventArgs<TState, TData>(transition, e, data));
                }
                else
                {                    
                    throw e;
                }

                return null;
            }
        }

        private void HandleHaltRequest()
        {
            if (HaltRequestIssued != null) HaltRequestIssued(this, EventArgs.Empty);
        }

        private void HandleWaitRequest()
        {
            lock (_lockObject)
            {                
                IsWaiting = true;                
            }

            if (_resumeQueue.Count > 0)
            {
                Tuple<TState, TData> nextRequest = _resumeQueue.Dequeue();

                if (ResumeRequestDequeued != null) ResumeRequestDequeued(this, new EventArgs<TState, TData>(nextRequest.Item1, nextRequest.Item2));

                Resume(nextRequest.Item1, nextRequest.Item2);
            }

            if (WaitRequestStarted != null) WaitRequestStarted(this, EventArgs.Empty);
        }

        private void HandleCompleteTransitionRequest(TState finalState, TData data)
        {
            if (TransitionsCompleted != null) TransitionsCompleted(this, new EventArgs<TState, TData>(finalState, data));
        }        

        #endregion

        private void HandleUnhandledTransitionAttempt(Transition<TState> transition)
        {
            if (!AllowUnhandledTransitions) throw new InvalidOperationException("Transition from state '{0}' to '{1}' not handled".F(PreviousState, transition.To));

            if (UnhandledTransitionOccured != null) UnhandledTransitionOccured(this, new EventArgs<Transition<TState>>(transition));
        }

        public static Transition<TState> CreateTransition(TState fromState, TState toState)
        {
            Transition<TState> transition = new Transition<TState>(fromState, toState);

            return transition;
        }

        /// <summary>
        /// Short hand alias for GetRequest (T == TransitionRequest)
        /// </summary>
        public TransitionRequest<TState, TData> T(TState state, TData data)
        {
            return GetRequest(state, data);
        }

        public TransitionRequest<TState, TData> GetRequest(TState state, TData data)
        {
            return TransitionRequest<TState, TData>.For(state, data);
        }

        public bool AllowUnhandledTransitions { get; set; }
        public bool AllowDatalessTransitions { get; set; }
        public bool AutoTransitionToErrorState { get; set; }
        public bool AllowQueuedResumeRequests { get; set; }

        public TState ErrorState { get; set; }
    }

    public class StateObserver<TState> : StateObserver<TState, object> where TState : struct, IConvertible
    {
        public StateObserver(TState defaultState)
            : base(defaultState)
        {
        }

        public TransitionRequest<TState> GetRequest(TState state)
        {
            return TransitionRequest<TState>.For(state);
        }


        /// <summary>
        /// Shorthand alias for GetRequest (T == TransitionRequest)
        /// </summary>
        /// <param name="state"></param>
        /// <returns></returns>
        public TransitionRequest<TState> T(TState state) { return GetRequest(state); }
    }
}
